#!/usr/bin/env python
#
# Copyright 2009 Benjamin Davis
#
# This file is part of resistencia.
#
# resistencia is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# resistencia is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with resistencia.  If not, see <http://www.gnu.org/licenses/>.

import socket
import sys
import curses
import curses.textpad

from threading import Thread, Event, Lock

class ServerInput(Thread):
    
    def __init__(self, socket, parent):
        Thread.__init__(self)
        self.socket = socket
        self.parent = parent
        # The amount of readback buffer to store
        self.buffer_len = 4096
        # Get the window size
        height, width = self.parent.getmaxyx()
        # Create a new pad of the same width as the window
        self.window = curses.newpad(self.buffer_len, width)
        # The line on the buffer to echo data to
        self.line = 0
        self.stopEvent = Event()
        self.lock = Lock()
        
    def run(self):
        # While we haven't been told to stop
        while not self.stopEvent.isSet():
            try:
                # Try to get data from server
                data = self.socket.recv(1024)
                if not data:
                    # If no data is received, server has disconnected
                    # Stop the thread
                    self.stopEvent.set()
                    self.echo("Server disconnected")
                else:
                    # Otherwise, output the data to screen..
                    self.echo(data)
            # self.socket.recv will timeout every 100ms to check if
            # stopEvent is set
            except socket.timeout:
                pass
    
    def echo(self, data):
        """Outputs formatted data to self.window
        
        Automatically inserts new lines so that no word is spread across
        multiple lines."""
        self.lock.acquire()
        height, width = self.parent.getmaxyx()
        lines = []
        # Iterate through the lines in data
        for string in data.splitlines():
            # If 'string' is still longer than the window width
            while len(string) > width:
                # First of all, take a cut equal to the width of the screen
                # Then find the right-most space and add all the data before
                # it to 'lines' as a new string
                lines.append(string[:string[:width].rfind(' ')])
                # Subtract the line from 'string'
                string = string[string[:width].rfind(' '):].strip()
            # If 'string' is shorter than the window width, just append it
            # to 'lines'
            else:
                lines.append(string)
        self.lock.release()
        
        # Write all lines to screen in sequence
        for line in lines:
            self.insert(line)
            
    def insert(self, data):
        """Writes any data passed to self.window"""
        # Write data to buffer
        self.window.insstr(self.line, 0, data)
        height, width = self.parent.getmaxyx()
        # Refresh the pad onto the screen
        self.window.refresh(self.line - height + 2, 0, 0, 0, height - 2, width)
        # If we're at the end of the buffer
        if self.line == self.buffer_len - 1:
            # Delete the top (first) line
            self.window.move(0, 0)
            self.window.deleteln()
        # If we're not at the end of the buffer
        else:
            self.line += 1

    def join(self):
        """Called to stop the thread"""
        self.stopEvent.set()
        Thread.join(self)
        
class Input(object):
    
    def __init__(self, socket, parent, output):
        self.socket = socket
        self.parent = parent
        self.output = output
        height, width = self.parent.getmaxyx()
        self.create(height, width)

    def run(self):
        while True:
            # Get data from user
            data = self.window.getstr(0, 0)
            # Clear the input box
            self.window.erase()
            height, width = self.parent.getmaxyx()
            # If the user resizes the window
            if (height - 1, 0) != self.window.getbegyx():
                # Recreate the input box with new size
                self.create(height, width)
            # If user entered a non-empty string
            if data != '':
                # Send it to the server
                self.socket.send(data)
    
    def create(self, height, width):
        """Creates self.window as a subwin of self.parent with a height of 1"""
        self.window = self.parent.subwin(1, width, height - 1, 0)
        # Let curses handle escape-sequences (keypad, backspace, etc.)
        self.window.keypad(1)
        
def main(stdscr, addr):
    
    try:
        # Echo what the user writes to screen
        curses.echo()
        # Use the default terminal colours
        curses.use_default_colors()
        
        # Create a new TCP/IP socket
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        # Connect to the server
        s.connect(addr)
        # Set a timeout of 100ms (see ServerInput.run for rationale)
        s.settimeout(0.1)
        
        # Create server output window
        output = ServerInput(s, stdscr)
        # Start listening for data from the server
        output.start()
        
        # Create input box
        Input(s, stdscr, output).run()
        
    except (socket.timeout, socket.error):
        # Close the socket
        s.close()
        raise
        
    except:
        # Stop getting data from the server
        output.join()
        raise

if __name__ == "__main__":
    try:
        # Try to get host from supplied parameters
        HOST = sys.argv[1]
    # If user didn't supply a host
    except IndexError:
        # Default to localhost
        HOST = 'localhost'
    
    try:
        # Try to get port from supplied parameters
        PORT = int(sys.argv[2])
    # If user didn't supply a port
    except (IndexError, ValueError):
        # Default to 4096
        PORT = 4096
    
    try:
        # curses.wrapper will clean up curses library upon exception
        curses.wrapper(main, (HOST, PORT))
    # If user passes a keyboard interrupt, exit silently
    except KeyboardInterrupt:
        pass
    # On socket errors, give more user-friendly error messages
    except socket.timeout:
        print "Connection timed out."
    except socket.error:
        print "Connection refused."
